/*
 * Copyright 2020-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package tech.firas.tool;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class Parser {

    private String singleLineCommentBegin;
    private String multiLineCommentBegin;
    private String multiLineCommentEnd;
    private String stringBeginRegex;
    private String stringEndRegex;
    // does not support multi-line String yet

    public static List<ParsedContent> filterOutComment(final List<ParsedContent> src) {
        final List<ParsedContent> result = new LinkedList<>();
        for (final ParsedContent item : src) {
            if (!Type.MULTI_LINE_COMMENT.equals(item.getType()) && !Type.SINGLE_LINE_COMMENT.equals(item.getType())) {
                result.add(item);
            }
        }
        return result;
    }

    public static List<ParsedContent> trimOther(final List<ParsedContent> src) {
        final List<ParsedContent> result = new ArrayList<>(src.size());
        for (final ParsedContent item : src) {
            if (Type.OTHER.equals(item.getType())) {
                final ParsedContent dest = new ParsedContent(item.getBeginRow(), item.getBeginColumn(), Type.OTHER);
                dest.setEnd(item.getEndRow(), item.getEndColumn(),
                        item.getContent().replaceFirst("^\\s+", " ").replaceFirst("\\s+$", " "));
                result.add(dest);
            } else {
                result.add(item);
            }
        }
        return result;
    }

    public enum Type {
        SINGLE_LINE_COMMENT,
        MULTI_LINE_COMMENT,
        STRING,
        OTHER
    }

    protected String getSingleLineCommentBegin() {
        return singleLineCommentBegin;
    }

    protected void setSingleLineCommentBegin(final String singleLineCommentBegin) {
        this.singleLineCommentBegin = singleLineCommentBegin;
    }

    protected String getMultiLineCommentBegin() {
        return multiLineCommentBegin;
    }

    protected void setMultiLineCommentBegin(final String multiLineCommentBegin) {
        this.multiLineCommentBegin = multiLineCommentBegin;
    }

    protected String getMultiLineCommentEnd() {
        return multiLineCommentEnd;
    }

    protected void setMultiLineCommentEnd(final String multiLineCommentEnd) {
        this.multiLineCommentEnd = multiLineCommentEnd;
    }

    protected String getStringBeginRegex() {
        return stringBeginRegex;
    }

    protected void setStringBeginRegex(final String stringBeginRegex) {
        this.stringBeginRegex = stringBeginRegex;
    }

    protected String getStringEndRegex() {
        return stringEndRegex;
    }

    protected void setStringEndRegex(final String stringEndRegex) {
        this.stringEndRegex = stringEndRegex;
    }

    public List<ParsedContent> parse(final Scanner scanner) {
        final List<ParsedContent> result = new LinkedList<>();
        int rowNum = 0;
        ParsedContent currentContent = new ParsedContent();
        while (scanner.hasNextLine()) {
            rowNum += 1;
            final String row = scanner.nextLine();
            switch (currentContent.getType()) {
                case OTHER:
                    currentContent = this.handleOther(result, row, rowNum, currentContent);
                    break;
                case STRING:
                    currentContent = this.handleString(result, row, rowNum, currentContent);
                    break;
                case MULTI_LINE_COMMENT:
                    currentContent = this.handleMultiLineComment(result, row, rowNum, currentContent);
                    break;
            }
            if (!scanner.hasNextLine()) { // input ends
                currentContent.setEnd(rowNum, row.length(), "");
                result.add(currentContent);
            }
        }

        // remove empty content
        for (final Iterator<ParsedContent> iterator = result.iterator(); iterator.hasNext(); ) {
            final ParsedContent parsedContent = iterator.next();
            if (parsedContent.getBeginRow() >= parsedContent.getEndRow() &&
                    parsedContent.getBeginColumn() >= parsedContent.getEndColumn()) {
                iterator.remove();
            }
        }
        return result;
    }

    private ParsedContent handleOther(final List<ParsedContent> result, final String row, final int rowNum,
            final ParsedContent currentContent) {
        final Pattern stringBeginPattern = Pattern.compile(this.stringBeginRegex);

        final int beginColumn = rowNum == currentContent.getBeginRow() ? currentContent.getBeginColumn() : 0;
        int singleLineCommentIndex = this.singleLineCommentBegin == null ? -1 :
                row.indexOf(this.singleLineCommentBegin, beginColumn);
        int multiLineCommentIndex = this.multiLineCommentBegin == null ? -1 :
                row.indexOf(this.multiLineCommentBegin, beginColumn);
        final Matcher stringBeginMatcher = stringBeginPattern.matcher(row);
        if (stringBeginMatcher.find(beginColumn)) {
            final int stringIndex = stringBeginMatcher.start();
            if (singleLineCommentIndex < 0 || stringIndex <= singleLineCommentIndex) {
                if (multiLineCommentIndex < 0 || stringIndex <= multiLineCommentIndex) {
                    final String toAppend = rowNum == currentContent.getBeginRow() ?
                            row.substring(currentContent.getBeginColumn(), stringIndex) : '\n' + row.substring(0, stringIndex);
                    currentContent.setEnd(rowNum, stringIndex, toAppend);
                    result.add(currentContent);

                    final ParsedContent newContent = new ParsedContent(rowNum, stringIndex, Type.STRING);
                    newContent.setStringBegin(stringBeginMatcher.group());
                    return this.handleString(result, row, rowNum, newContent);
                } else {
                    // 0 <= multiLineCommentIndex < stringIndex < singleLineCommentIndex
                    return this.beginMultiLineComment(result, row, rowNum, currentContent, multiLineCommentIndex);
                }
            } else {
                // 0 <= singleLineCommentIndex < stringIndex
                if (multiLineCommentIndex < 0 || singleLineCommentIndex <= multiLineCommentIndex) {
                    return this.beginSingleLineComment(result, row, rowNum, currentContent, singleLineCommentIndex);
                } else {
                    // 0 <= multiLineCommentIndex < singleLineCommentIndex < stringIndex
                    return this.beginMultiLineComment(result, row, rowNum, currentContent, multiLineCommentIndex);
                }
            }
        } else if (singleLineCommentIndex >= 0) {
            if (multiLineCommentIndex < 0 || singleLineCommentIndex <= multiLineCommentIndex) {
                return this.beginSingleLineComment(result, row, rowNum, currentContent, singleLineCommentIndex);
            } else {
                // 0 <= multiLineCommentIndex < singleLineCommentIndex
                return this.beginMultiLineComment(result, row, rowNum, currentContent, multiLineCommentIndex);
            }
        } else if (multiLineCommentIndex >= 0) {
            return this.beginMultiLineComment(result, row, rowNum, currentContent, multiLineCommentIndex);
        } else {
            currentContent.append(rowNum == currentContent.getBeginRow() ? row.substring(beginColumn) : '\n' + row);
            return currentContent;
        }
    }

    private ParsedContent beginSingleLineComment(final List<ParsedContent> result, final String row, final  int rowNum,
            final ParsedContent currentContent, final int singleLineCommentIndex) {
        final String toAppend = rowNum == currentContent.getBeginRow() ?
                row.substring(currentContent.getBeginColumn(), singleLineCommentIndex) :
                '\n' + row.substring(0, singleLineCommentIndex);
        currentContent.setEnd(rowNum, singleLineCommentIndex, toAppend);
        result.add(currentContent);

        final ParsedContent newContent = new ParsedContent(rowNum, singleLineCommentIndex, Type.SINGLE_LINE_COMMENT);
        newContent.setEnd(rowNum, row.length(), row.substring(singleLineCommentIndex));
        result.add(newContent);

        return new ParsedContent(rowNum, row.length(), Type.OTHER);
    }

    private ParsedContent beginMultiLineComment(final List<ParsedContent> result, final String row, final  int rowNum,
            final ParsedContent currentContent, final int multiLineCommentIndex) {
        final String toAppend = rowNum == currentContent.getBeginRow() ?
                row.substring(currentContent.getBeginColumn(), multiLineCommentIndex) :
                '\n' + row.substring(0, multiLineCommentIndex);
        currentContent.setEnd(rowNum, multiLineCommentIndex, toAppend);
        result.add(currentContent);

        return this.handleMultiLineComment(result, row, rowNum,
                new ParsedContent(rowNum, multiLineCommentIndex, Type.MULTI_LINE_COMMENT));
    }

    private ParsedContent handleMultiLineComment(final List<ParsedContent> result, final String row, final  int rowNum,
            final ParsedContent currentContent) {
        if (rowNum == currentContent.getBeginRow()) {
            final int index = row.indexOf(this.multiLineCommentEnd,
                    currentContent.getBeginColumn() + this.multiLineCommentBegin.length());
            if (index >= 0) {
                final int endIndex = index + this.multiLineCommentEnd.length();
                currentContent.setEnd(rowNum, endIndex, row.substring(currentContent.getBeginColumn(), endIndex));
                result.add(currentContent);
                return this.handleOther(result, row, rowNum, new ParsedContent(rowNum, endIndex, Type.OTHER));
            } else { // the comment does not end at the same line
                currentContent.append(row.substring(currentContent.getBeginColumn()));
            }
        } else {
            final int index = row.indexOf(this.multiLineCommentEnd);
            if (index >= 0) {
                final int endIndex = index + this.multiLineCommentEnd.length();
                currentContent.setEnd(rowNum, endIndex, '\n' + row.substring(0, endIndex));
                result.add(currentContent);
                return this.handleOther(result, row, rowNum, new ParsedContent(rowNum, endIndex, Type.OTHER));
            } else {
                currentContent.append('\n' + row);
            }
        }
        return currentContent;
    }

    private ParsedContent handleString(final List<ParsedContent> result, final String row, final int rowNum,
            final ParsedContent currentContent) {
        final Pattern stringEndPattern = Pattern.compile(
                this.stringEndRegex.replace("$BEGIN", currentContent.getStringBegin()));

        if (rowNum == currentContent.getBeginRow()) {
            final Matcher matcher = stringEndPattern.matcher(row.substring(currentContent.getBeginColumn() + 1));
            if (matcher.find()) {
                final int endIndex = currentContent.getBeginColumn() + matcher.end() + 1;
                currentContent.setEnd(rowNum, endIndex, row.substring(currentContent.getBeginColumn(), endIndex));
                result.add(currentContent);
                return this.handleOther(result, row, rowNum, new ParsedContent(rowNum, endIndex, Type.OTHER));
            } else {
                currentContent.append(row.substring(currentContent.getBeginColumn()));
            }
        } else {
            final Matcher matcher = stringEndPattern.matcher(row);
            if (matcher.find()) {
                final int endIndex = matcher.end();
                currentContent.setEnd(rowNum, endIndex, '\n' + row.substring(0, endIndex));
                result.add(currentContent);
                return this.handleOther(result, row, rowNum, new ParsedContent(rowNum, endIndex, Type.OTHER));
            } else {
                currentContent.append('\n' + row);
            }
        }
        return currentContent;
    }

    public static class ParsedContent implements Serializable {

        private int beginRow = 1;
        private int beginColumn = 0;
        private int endRow = 0;
        private int endColumn = 0;
        private Type type = Type.OTHER;
        private String stringBegin = null; // only valid when type == Type.STRING
        private StringBuilder contentBuilder = new StringBuilder();

        public ParsedContent() {}

        public ParsedContent(final int beginRow, final int beginColumn, final Type type) {
            this.setBeginRow(beginRow);
            this.setBeginColumn(beginColumn);
            this.setType(type);
        }

        public int getBeginRow() {
            return beginRow;
        }

        public void setBeginRow(final int beginRow) {
            if (beginRow < 1) {
                throw new IllegalArgumentException("beginRow < 1: " + beginRow);
            }
            this.beginRow = beginRow;
        }

        public int getBeginColumn() {
            return beginColumn;
        }

        public void setBeginColumn(final int beginColumn) {
            if (beginColumn < 0) {
                throw new IllegalArgumentException("beginColumn < 0: " + beginColumn);
            }
            this.beginColumn = beginColumn;
        }

        public int getEndRow() {
            return endRow;
        }

        public void setEndRow(final int endRow) {
            if (endRow < 0) {
                throw new IllegalArgumentException("endRow < 0: " + endRow);
            }
            this.endRow = endRow;
        }

        public int getEndColumn() {
            return endColumn;
        }

        public void setEndColumn(final int endColumn) {
            if (endColumn < 0) {
                throw new IllegalArgumentException("endColumn < 0: " + endColumn);
            }
            this.endColumn = endColumn;
        }

        public void setEnd(final int endRow, final int endColumn, final String toAppend) {
            this.setEndRow(endRow);
            this.setEndColumn(endColumn);
            this.append(toAppend);
        }

        public Type getType() {
            return type;
        }

        public void setType(final Type type) {
            if (type == null) {
                throw new IllegalArgumentException("type is null");
            }
            this.type = type;
        }

        public String getStringBegin() {
            return stringBegin;
        }

        public void setStringBegin(final String stringBegin) {
            this.stringBegin = stringBegin;
        }

        public void append(final String toAppend) {
            this.contentBuilder.append(toAppend);
        }

        public String getContent() {
            return this.contentBuilder.toString();
        }
    }
}
